import pandas as pd  # 导入数据处理库 Pandas
import numpy as np  # 导入数值计算库 NumPy
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_log_error
import matplotlib.pyplot as plt # 导入 matplotlib 中的 plt 函数

# 读取玩家角色表数据
roles = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_id.csv')
# 创建一个空列表用于存放不同天数的角色数据
dfs = []
# 针对七天的数据进行循环
for i in range(2, 9):
    # 复制角色表数据至临时变量 tmp
    tmp = roles.copy()
    # 在临时变量中添加一个名为 'day' 的列，并赋值为当前的天数 i
    tmp['day'] = i
    # 将处理后的临时数据添加至列表 dfs 中
    dfs.append(tmp)
# 将所有临时数据拼接成一个完整的数据集，并重新设置索引
data = pd.concat(dfs).reset_index(drop=True)
# 打印data前三行数据的代码
#print(data.head(3))

##以上代码主要是读取玩家角色表数据，并通过循环创建了一个包含七天数据的 data 数据集。"

# 读取货币消耗表数据
consume = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_consume_op.csv')
# 将 'dt' 列转换为 Pandas 的日期时间格式
consume['dt'] = pd.to_datetime(consume['dt'])
# 从日期时间中提取天数，并创建一个名为 'day' 的新列
consume['day'] = consume['dt'].dt.day
# 删除 "dt" 列，并将结果赋值回 consume
consume = consume.drop('dt', axis=1)
# 删除 "level" 列，并将结果赋值回 consume，因为我主观上认为level和充值金额间没显著联系
consume = consume.drop('level', axis=1)
"以上代码主要读取了货币消耗表的数据，并对日期时间进行了处理，提取了每条记录对应的日期中的天数，并将结果保存在新创建的 'day' 列中。"
#print(consume.head(3))


# 对货币消耗表进行处理并合并到 data 数据中
for i in range(1, 5):  # 针对特定范围内的数据列进行循环处理
    for m in ['count', 'sum']:  # 针对 'use_t{i}' 列的计数和求和进行循环处理
        # 根据 'role_id' 和 'day' 分组计算 'use_t{i}' 列的聚合统计，并重新设置列名
        tmp = consume.groupby(['role_id', 'day'])[f'use_t{i}'].agg(m).to_frame(name=f'use_t{i}_{m}').reset_index()
        # 将新计算的数据根据 'role_id' 和 'day' 合并到原始 data 数据中
        data = data.merge(tmp, on=['role_id', 'day'], how='left')


# 检查合并后数据表的数据
#print(data.head())


# 读取角色进化表数据并处理
evolve = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_evolve_op.csv')
evolve['dt'] = pd.to_datetime(evolve['dt'])  # 将日期列转换为 Pandas 的日期时间格式
evolve['day'] = evolve['dt'].dt.day  # 提取日期中的天数，并创建 'day' 列

# 计算角色升级差值和重命名列名
evolve['n_level_up'] = evolve['new_lv'] - evolve['old_lv']
evolve = evolve.rename(columns={'num': 'lv_consume_item_num'})
#print(evolve.head())

# 计算每个角色每天升级了多少次
tmp = evolve.groupby(['role_id', 'day'])['type'].size().reset_index(name='upgrade_type_counts')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算每个角色每天升级道具使用了多少次
tmp = evolve.groupby(['role_id', 'day'])['item_id'].size().reset_index(name='item_usage_counts')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算每个角色每天升了多少级
tmp = evolve.groupby(['role_id', 'day'])['n_level_up'].sum().reset_index(name='total_levels_upgraded')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算每个角色每天平均升了多少级
tmp = evolve.groupby(['role_id', 'day'])['n_level_up'].mean().reset_index(name='average_levels_upgraded')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算每个角色每天升级道具一共使用的数量
tmp = evolve.groupby(['role_id', 'day'])['lv_consume_item_num'].sum().reset_index(name='total_items_used')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算每个角色每天升级道具平均使用的数量
tmp = evolve.groupby(['role_id', 'day'])['lv_consume_item_num'].mean().reset_index(name='average_items_used')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

#print(data.head())
"以上代码块主要涉及了对货币消耗表和角色进化表的处理"
"提取了各种统计信息，例如消耗次数、消耗数量、升级次数等，并将这些统计信息合并到了 data 数据中。"


# 读取副本表数据并进行日期处理
fb = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_fb_op.csv')
fb['dt'] = pd.to_datetime(fb['dt'])  # 将日期列转换为 Pandas 的日期时间格式
fb['day'] = fb['dt'].dt.day  # 提取日期中的天数，并创建 'day' 列
fb['fb_used_time'] = fb['finish_time'] - fb['start_time']  # 计算副本的使用时间
#print(fb.head())

# 计算某角色某天打副本的总尝试次数
tmp = fb.groupby(['role_id', 'day'])['fb_id'].size().reset_index(name='total_attempts')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算某角色某天打副本的平均用时
tmp = fb.groupby(['role_id', 'day'])['fb_used_time'].mean().reset_index(name='average_time_fb')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算某角色某天打副本的总用时
tmp = fb.groupby(['role_id', 'day'])['fb_used_time'].sum().reset_index(name='total_time_fb')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算某角色某天打副本获得的平均经验
tmp = fb.groupby(['role_id', 'day'])['exp'].mean().reset_index(name='average_exp_fb')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 计算某角色某天打副本获得的总经验
tmp = fb.groupby(['role_id', 'day'])['exp'].sum().reset_index(name='total_exp_fb')
data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 使用for循环遍历一个包含元素0, 1, 2的列表。在循环中，通过筛选出fb_result等于当前循环变量i的子DataFrame，
# 将该子DataFrame的列名修改为除最后一列外的所有列名加上'fb_result{i}_count'。这样可以为每个fb_result的取值创建一个对应的列。
tmp = fb.groupby(['role_id', 'day'])['fb_result'].value_counts().reset_index(name='fb_result_count')
#print(tmp)
for i in [0, 1, 2]:
    tt = tmp[tmp['fb_result'] == i]
    tt.columns = list(tt.columns[:-1]) + ['fb_result%d_count' % i]
    data = data.merge(tt[['role_id', 'day', 'fb_result%d_count' % i]], on=['role_id', 'day'], how='left')


# 检查合并后数据表的数据
#print(data.head())

"以上代码块主要涉及了对玩家副本挑战表的处理，提取了各种统计信息到了 data 数据中。"


# 读取任务系统表数据并进行日期处理
mission = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_mission_op.csv')
mission['dt'] = pd.to_datetime(mission['dt'])  # 将日期列转换为 Pandas 的日期时间格式
mission['day'] = mission['dt'].dt.day  # 提取日期中的天数，并创建 'day' 列

# 针对特定列进行循环处理，计算其统计信息并合并到 data 数据中
for col in ['mission_id', 'mission_type']:
    for m in ['count', 'nunique']:  # 计算计数和唯一值数量
        tmp = mission.groupby(['role_id', 'day'])[col].agg(m).to_frame(name=f'{col}_day_{m}').reset_index()
        data = data.merge(tmp, on=['role_id', 'day'], how='left')
        
# 检查合并后数据表的数据
#print(data.head())
"以上代码块主要涉及了对玩家任务系统表的处理，提取了各种统计信息到了 data 数据中。"


# 读取玩家离线数据并进行日期处理
offline = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_offline_op.csv')
offline['dt'] = pd.to_datetime(mission['dt'])  # 将日期列转换为 Pandas 的日期时间格式
offline['day'] = offline['dt'].dt.day  # 提取日期中的天数，并创建 'day' 列
offline['online_durations'] = offline['offline'] - offline['online']  # 计算在线时长

# 针对特定列进行循环处理，计算其统计信息并合并到 data 数据中
for col in ['reason', 'map_id']:
    for m in ['count', 'nunique']:  # 计算计数和唯一值数量
        tmp = offline.groupby(['role_id', 'day'])[col].agg(m).to_frame(name=f'{col}_day_{m}').reset_index()
        data = data.merge(tmp, on=['role_id', 'day'], how='left')

# 针对特定列进行循环处理，计算其统计信息并合并到 data 数据中
for col in ['online_durations']:
    for m in ['mean', 'sum']:  # 计算平均值和求和
        tmp = offline.groupby(['role_id', 'day'])[col].agg(m).to_frame(name=f'{col}_day_{m}').reset_index()
        data = data.merge(tmp, on=['role_id', 'day'], how='left')
#print(data.head())


# 读取付费表数据并进行日期处理
pay = pd.read_csv(r'D:\作业\机器学习\CCF BDCI\role_pay.csv')
pay['dt'] = pd.to_datetime(pay['dt'])  # 将日期列转换为 Pandas 的日期时间格式
pay['day'] = pay['dt'].dt.day  # 提取日期中的天数，并创建 'day' 列

# 按照角色和天数进行分组，计算每天的付费金额总和
tmp = pay.groupby(['role_id', 'day'])['pay'].agg('sum').to_frame(name='pay_sum_day').reset_index()

# 将每天的付费金额总和合并到 data 数据中
data = data.merge(tmp, on=['role_id', 'day'], how='left')

#print(data.head())


# 对data数据集中的所有缺失值进行处理，处理方法为全置0
# 打印data中缺失值的数量
#print(data.isnull().sum())
# 缺失我们把其当作0即可
data = data.fillna(0) 
# 打印data中缺失值的数量
#print(data.isnull().sum())


# 创建初步训练集：使用前 6 天的行为预测第 7 天的付费情况
# 创建一个空的DataFrame对象df_train，并将一个列名为’role_id’的列添加到其中。列的值来自于数据集data中的role_id列去重后得到的唯一值列表。
df_train = pd.DataFrame({'role_id': data.role_id.unique().tolist()})
for i, d in enumerate(range(2, 8)):
    #根据data中的day列的值等于d的条件，筛选出满足条件的子数据集，并将其赋值给临时变量tmp
    tmp = data[data.day == d].copy().reset_index(drop=True)
    #丢弃create_time、day两列 axis=1表示删除列，inplace=True表示在原数据上进行操作
    tmp.drop(['create_time', 'day'], axis=1, inplace=True)
    tmp.columns = ['role_id'] + [f'{c}_day{i}' for c in tmp.columns[1:]]
    df_train = df_train.merge(tmp, on='role_id')

#print(df_train.head())


# 创建最终训练集
# 从付费表中提取初步训练集的标签
day8_pay = pay[pay.day == 8].copy().reset_index(drop=True)
tmp = day8_pay.groupby('role_id')['pay'].agg('sum').to_frame(name='seventh_pay').reset_index()
df_train = df_train.merge(tmp, on='role_id', how='left')
df_train['seventh_pay'].fillna(0., inplace=True)  # 对于缺失值填充为 0.0
#print(df_train.head())


# 集中处理最终训练集

# 首先，由于每个用户id不同，训练时必须把id剔除。否则这类特征通常是高基数分类特征，这意味着它们包含大量唯一项。
# 对此类特征进行独热编码通常不可行，因为这会生成大量的特征，导致维度过高，这个问题也被称为“维度灾难”
#剔除最终训练集中的role_id
df_train = df_train.drop(columns=['role_id'])

# 定义一个异常值处理函数
def handle_outliers(df, method='zscore', threshold=3):
    numerical_features = df.select_dtypes(include=[np.number])  # 选择数值型特征
    if method == 'zscore':
        # 使用Z-score方法识别异常值
        for feature in numerical_features:
            df_feature = df[feature]
            z_scores = (df_feature - df_feature.mean()) / df_feature.std()
            outliers = abs(z_scores) > threshold
            df[feature][outliers] = df_feature.median()  # 用中位数替换异常值
    elif method == 'iqr':
        # 使用四分位数间距(IQR)方法识别异常值
        for feature in numerical_features:
            df_feature = df[feature]
            Q1 = df_feature.quantile(0.25)
            Q3 = df_feature.quantile(0.75)
            IQR = Q3 - Q1
            lower_bound = Q1 - (1.5 * IQR)
            upper_bound = Q3 + (1.5 * IQR)
            df[feature] = np.where((df_feature < lower_bound), df_feature.median(), df_feature)  # 下界异常值替换
            df[feature] = np.where((df_feature > upper_bound), df_feature.median(), df_feature)  # 上界异常值替换
    else:
        raise ValueError("Invalid method. Use 'zscore' or 'iqr'.")
    return df

# 在df_train数据集上应用异常值处理：
df_train = handle_outliers(df_train, method='zscore', threshold=3)
# df_train 现在是处理过异常值的DataFrame

#print(df_train.head())



# 最终训练集拆分，分为训练集和验证集，80%训练集和20%验证集
# 分离特征和目标变量
X = df_train.drop(columns=['seventh_pay'])  # 特征数据
y = df_train['seventh_pay']                # 目标变量

# 拆分数据集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.20, random_state=42)

# 查看数据维度来确认拆分
#print("训练集特征数据的维度：", X_train.shape)
#print("验证集特征数据的维度：", X_val.shape)
#print("训练集目标变量的维度：", y_train.shape)
#print("验证集目标变量的维度：", y_val.shape)


# 开始训练

def train(X_train, y_train, X_val, y_val):
    """
    使用 RandomForestRegressor 训练模型并评估性能。
    
    参数:
    X_train, y_train -- 训练集特征和目标变量
    X_val, y_val -- 验证集特征和目标变量
    n_estimators -- 树的数量
    max_features -- 寻找最佳分割时要考虑的特征数量
    max_depth -- 最大树深
    min_samples_split -- 内部节点再划分所需最小样本数
    min_samples_leaf -- 叶节点最少样本数
    bootstrap -- 是否采用bootstrap采样
    
    返回:
    model -- 训练好的模型
    """
    # 创建随机森林回归器实例
    model = RandomForestRegressor(n_estimators=500, random_state=42, bootstrap=False, max_depth=12, 
                                  max_features='sqrt', min_samples_leaf=2, min_samples_split=2)

    # 训练模型
    model.fit(X_train, y_train)
    
    # 预测验证集
    y_pred = model.predict(X_val)
    
    # 计算并打印性能指标-均方对数误差
    msle = mean_squared_log_error(y_val, y_pred)
    print(f'验证集的均方对数误差（MSLE）: {msle:.2f}')

    # 计算分数
    Score = 1/(1+msle)
    print('均方对数误差分值：',Score)

    # 绘制预测值和真实值的对比图
    plt.figure(figsize=(10,5))
    plt.scatter(range(len(y_val)), y_val, color='blue', label='Actual values')
    plt.scatter(range(len(y_pred)), y_pred, color='red', label='Predicted values', alpha=0.5)
    plt.title('Comparison of Actual and Predicted Values')
    plt.xlabel('Sample index')
    plt.ylabel('Target value')
    plt.legend()
    plt.show()
    # 此段代码使用scatter图将预测值和实际值对比显示，蓝色点表示真实值，红色点表示预测值。
 
    # 返回训练好的模型
    return model

# 然后使用训练集数据(X_train, y_train) 和验证集数据(X_val, y_val)调用训练函数
model = train(X_train, y_train, X_val, y_val)